package client;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.URL;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import view.MainWindow;

/**
 * Client: Manage the connection of the client to the server.
 *
 */
public class Client implements ClientInterface {

    private Client client;
    private final int BUFFERSIZE = 1; //Bytes of buffer to download or upload. (1 make the trasnfers slower and multiThreading can be seen.)

    private MainWindow mainWindow;
    private Socket socket;

    private String hostName;
    private int portNumber;

    private URL remoteFolder;
    private URL localFolder;

    private BufferedReader inputServer;
    private PrintWriter outputServer;

    /**
     * Consturctor.
     *
     * @param hostName String with the hostName of the server.
     * @param portNumber Integer server port.
     * @param mainWindow The view wicht the client interact.
     */
    public Client(String hostName, int portNumber, MainWindow mainWindow) {

        this.client = this;
        this.mainWindow = mainWindow;
        this.hostName = hostName;
        this.portNumber = portNumber;

    }

    @Override
    public synchronized void connect() throws IOException { //Synchronized to avoid problems with Threads.

        this.socket = new Socket(hostName, portNumber);
        this.inputServer = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
        this.outputServer = new PrintWriter(this.socket.getOutputStream(), true);

    }

    @Override
    public synchronized void listRemoteFiles() { //Synchronized to avoid problems with Threads.

        try {
            outputServer.println("LIST");
            int port = Integer.parseInt(inputServer.readLine());
            Socket lSocket = new Socket(hostName, port);

            InputStream inS = lSocket.getInputStream();
            ObjectInputStream mapInputStream = new ObjectInputStream(inS);
            Map<File, Map> mapFiles = (Map<File, Map>) mapInputStream.readObject();

            mapInputStream.close();
            inS.close();
            lSocket.close();

            this.mainWindow.setTree(mapFiles);

        } catch (IOException ex) {
            System.err.println("Error listRemoteFiles. " + ex.getMessage());
        } catch (ClassNotFoundException ex) {
            System.err.println("Error listRemoteFiles . " + ex.getMessage());
        } catch (NullPointerException ex) {
            System.err.println("Error listRemoteFiles . " + ex.getMessage());
        }

    }

    @Override
    public void downloadFile(MainWindow mainWindow, String stringRelativePath, File localFile, int currentBar) {
        try {
            System.out.println("Downloading");
            outputServer.println("GET " + "\"" + stringRelativePath + "\""); //Send the command to the server.
            int port = Integer.parseInt(inputServer.readLine()); //Read the server response. Port where download.
            int size = Integer.parseInt(inputServer.readLine()); //Read the server response. Size of the file.
            new Thread(new DownloadFileThread(mainWindow, stringRelativePath, localFile, currentBar, port, size)).start(); //Launch the download on a new thread.
        } catch (IOException ex) {
            Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        }

    }

    /**
     * DownloadFileThread: Manage the downloads with threadsupport.
     */
    private class DownloadFileThread implements Runnable {

        private MainWindow mainWindow;
        private String stringRelativePath;
        private File localFile;
        private int currentBar;

        private int port;
        private long size;

        /**
         * Constructor.
         *
         * @param mainWindow The view window.
         * @param stringRelativePath The remote path.
         * @param localFile The local file where save the download.
         * @param currentBar The progress bar on the view.
         * @param port The server data port for the new socket.
         * @param size The remote file size.
         */
        public DownloadFileThread(MainWindow mainWindow, String stringRelativePath, File localFile, int currentBar, int port, long size) {

            this.mainWindow = mainWindow;
            this.stringRelativePath = stringRelativePath;
            this.localFile = localFile;
            this.currentBar = currentBar;
            this.port = port;
            this.size = size;
        }

        /**
         * run() method for thread support.
         */
        @Override
        public void run() {
            try {
                Socket dSocket = new Socket(hostName, this.port); //Connect to a new port for this download.
                this.mainWindow.addTransferBar();
                this.mainWindow.setTransferBarInfo(this.currentBar, "Download: " + this.stringRelativePath, 0, 0);
                int count; //Streams and vars needed for the download.
                byte[] buffer = new byte[BUFFERSIZE];
                localFile.createNewFile();
                InputStream is = dSocket.getInputStream();
                FileOutputStream fos = new FileOutputStream(localFile);
                BufferedOutputStream bos = new BufferedOutputStream(fos);
                long done = 0;
                while ((count = is.read(buffer)) > 0) { //Download loop
                    bos.write(buffer, 0, count);
                    bos.flush();
                    done += count;
                    this.mainWindow.setTransferBarInfo(this.currentBar, "Download: " + this.stringRelativePath, (int) ((done) * 100 / size), 0);

                }
                bos.close();//Closing Streams and show info.
                dSocket.close();
                System.out.println("Download done. " + this.stringRelativePath);

                this.mainWindow.setTransferBarInfo(this.currentBar, "Download: " + this.stringRelativePath, 100, 1);

            } catch (IOException ex) {
                System.err.println("Error DownloadFileThread. " + ex.getMessage());
            }
        }

    }

    @Override
    public void uploadFile(MainWindow mainWindow, String stringRelativePath, File localFile, int currentBar) {
        try {
            System.out.println("Uploading");
            System.out.println("PUT " + "\"" + stringRelativePath + "/" + localFile.getName() + "\"");
            outputServer.println("PUT " + "\"" + stringRelativePath + "/" + localFile.getName() + "\"");
            int port = Integer.parseInt(inputServer.readLine());
            new Thread(new UploadFileThread(mainWindow, stringRelativePath, localFile, currentBar, port)).start();
        } catch (IOException ex) {
            Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * UploadFileThread: Manage the uploads with threadsupport.
     */
    private class UploadFileThread implements Runnable {

        private MainWindow mainWindow;
        private String stringRelativePath;
        private File localFile;
        private int currentBar;

        private int port;
        private long size;

        /**
         * Constructor.
         *
         * @param mainWindow The view window.
         * @param stringRelativePath The remote path.
         * @param localFile The local file where save the download.
         * @param currentBar The progress bar on the view.
         * @param port The server data port for the new socket.
         */
        private UploadFileThread(MainWindow mainWindow, String stringRelativePath, File localFile, int currentBar, int port) {

            this.mainWindow = mainWindow;
            this.stringRelativePath = stringRelativePath;
            this.localFile = localFile;
            this.currentBar = currentBar;
            this.port = port;
            this.size = localFile.length();
        }

        /**
         * run() method for thread support.
         */
        @Override
        public void run() {
            try {
                Socket dSocket = new Socket(hostName, this.port);
                this.mainWindow.addTransferBar();
                this.mainWindow.setTransferBarInfo(this.currentBar, "Upload: " + this.localFile.getName(), 0, 0);

                int count;
                byte[] buffer = new byte[BUFFERSIZE];
                OutputStream outS = dSocket.getOutputStream();
                BufferedInputStream in = new BufferedInputStream(new FileInputStream(localFile));
                long done = 0;
                while ((count = in.read(buffer)) > 0) {
                    outS.write(buffer, 0, count);
                    outS.flush();
                    done += count;
                    this.mainWindow.setTransferBarInfo(this.currentBar, "Upload: " + this.localFile.getName(), (int) ((done) * 100 / size), 0);

                }
                dSocket.close();
                dSocket.close();
                System.out.println("Upload done. " + localFile.getAbsolutePath());
                this.mainWindow.setTransferBarInfo(this.currentBar, "Upload: " + this.localFile.getName(), 100, 1);

                client.listRemoteFiles();

            } catch (IOException ex) {
                Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
            }

        }
    }
}
